package com.itmuch.cloud.configs;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;

import org.apache.ibatis.io.VFS;
import org.apache.ibatis.logging.Log;
import org.apache.ibatis.logging.LogFactory;

/**
 * Modified DefaultVFS for handling nested jar.
 * <p>
 * <p>
 * <p>
 * https://github.com/mybatis/mybatis-3/issues/325
 * <p>
 * https://github.com/StripesFramework/stripes/issues/35
 *
 * @version 1.0
 * @since 1.0
 */
public class SpringBootExecutableJarVFS extends VFS {
    private static final Log log = LogFactory.getLog(SpringBootExecutableJarVFS.class);
    /**
     * The magic header that indicates a JAR (ZIP) file.
     */
    private static final byte[] JAR_MAGIC = {'P', 'K', 3, 4};
    @Override
    public boolean isValid() {
        return true;
    }
    @Override
    public List list(URL url, String path) throws IOException {
        InputStream is = null;
        try {
            List resources = new ArrayList();
// First, try to find the URL of a JAR file containing the requested resource. If a JAR
// file is found, then we'll list child resources by reading the JAR.
            URL jarUrl = findJarForResource(url);
            if (jarUrl != null) {
                is = jarUrl.openStream();
                if (log.isDebugEnabled()) {
                    log.debug("Listing" + url);
                }
                resources = listResources(new JarInputStream(is), path);
            } else {
                List children = new ArrayList();
                try {
                    if (isJar(url)) {
// Some versions of JBoss VFS might give a JAR stream even if the resource
// referenced by the URL isn't actually a JAR
                        is = url.openStream();
                        JarInputStream jarInput = new JarInputStream(is);
                        if (log.isDebugEnabled()) {
                            log.debug("Listing" + url);
                        }
                        for (JarEntry entry; (entry = jarInput.getNextJarEntry()) != null; ) {
                            if (log.isDebugEnabled()) {
                                log.debug("Jar entry:" + entry.getName());
                            }
                            children.add(entry.getName());
                        }
                        jarInput.close();
                    } else {
/*
* Some servlet containers allow reading from directory resources like a
* text file, listing the child resources one per line. However, there is no
* way to differentiate between directory and file resources just by reading
* them. To work around that, as each line is read, try to look it up via
* the class loader as a child of the current resource. If any line fails
* then we assume the current resource is not a directory.
*/
                        is = url.openStream();
                        BufferedReader reader = new BufferedReader(new InputStreamReader(is));
                        List lines = new ArrayList();
                        for (String line; (line = reader.readLine()) != null; ) {
                            if (log.isDebugEnabled()) {
                                log.debug("Reader entry:" + line);
                            }
                            lines.add(line);
                            if (getResources(path + "/" + line).isEmpty()) {
                                lines.clear();
                                break;
                            }
                        }
                        if (!lines.isEmpty()) {
                            if (log.isDebugEnabled()) {
                                log.debug("Listing" + url);
                            }
                            children.addAll(lines);
                        }
                    }
                } catch (FileNotFoundException e) {
/*
* For file URLs the openStream() call might fail, depending on the servlet
* container, because directories can't be opened for reading. If that happens,
* then list the directory directly instead.
*/
                    if ("file".equals(url.getProtocol())) {
                        File file = new File(url.getFile());
                        if (log.isDebugEnabled()) {
                            log.debug("Listing directory" + file.getAbsolutePath());
                        }
                        if (file.isDirectory()) {
                            if (log.isDebugEnabled()) {
                                log.debug("Listing" + url);
                            }
                            children = Arrays.asList(file.list());
                        }
                    } else {
// No idea where the exception came from so rethrow it
                        throw e;
                    }
                }
// The URL prefix to use when recursively listing child resources
                String prefix = url.toExternalForm();
                if (!prefix.endsWith("/")) {
                    prefix = prefix + "/";
                }
// Iterate over immediate children, adding files and recursing into directories
                for (Object child : children) {
                    String resourcePath = path + "/" + child.toString();
                    resources.add(resourcePath);
                    URL childUrl = new URL(prefix + child);
                    resources.addAll(list(childUrl, resourcePath));
                }
            }
            return resources;
        } finally {
            if (is != null) {
                try {
                    is.close();
                } catch (Exception e) {
// Ignore
                }
            }
        }
    }
    /**
     * List the names of the entries in the given {@link JarInputStream} that begin with the
     * <p>
     * specified {@code path}. Entries will match with or without a leading slash.
     *
     * @param jar  The JAR input stream
     * @param path The leading path to match
     * @return The names of all the matching entries
     * @throws IOException If I/O errors occur
     */
    protected List listResources(JarInputStream jar, String path) throws IOException {
// Include the leading and trailing slash when matching names
        if (!path.startsWith("/")) {
            path = "/" + path;
        }
        if (!path.endsWith("/")) {
            path = path + "/";
        }
// Iterate over the entries and collect those that begin with the requested path
        List resources = new ArrayList();
        for (JarEntry entry; (entry = jar.getNextJarEntry()) != null; ) {
            if (!entry.isDirectory()) {
// Add leading slash if it's missing
                String name = entry.getName();
                if (!name.startsWith("/")) {
                    name = "/" + name;
                }
// Check file name
                if (name.startsWith(path)) {
                    if (log.isDebugEnabled()) {
                        log.debug("Found resource:" + name);
                    }
// Trim leading slash
                    resources.add(name.substring(1));
                }
            }
        }
        return resources;
    }
    /**
     * Attempts to deconstruct the given URL to find a JAR file containing the resource referenced
     * <p>
     * by the URL. That is, assuming the URL references a JAR entry, this method will return a URL
     * <p>
     * that references the JAR file containing the entry. If the JAR cannot be located, then this
     * <p>
     * method returns null.
     *
     * @param url The URL of the JAR entry.
     * @return The URL of the JAR file, if one is found. Null if not.
     * @throws MalformedURLException
     */
    protected URL findJarForResource(URL url) throws MalformedURLException {
        if (log.isDebugEnabled()) {
            log.debug("Find JAR URL:" + url);
        }
        if (isNestedJar(url)) {
// Retain jar: protocol as a workaround for #325
            if (log.isDebugEnabled()) {
                log.debug("It is a nested JAR:" + url);
            }
        } else {
// If the file part of the URL is itself a URL, then that URL probably points to the JAR
            try {
                for (; ; ) {
                    url = new URL(url.getFile());
                    if (log.isDebugEnabled()) {
                        log.debug("Inner URL:" + url);
                    }
                }
            } catch (MalformedURLException e) {
// This will happen at some point and serves as a break in the loop
            }
        }
// Look for the .jar extension and chop off everything after that
        StringBuilder jarUrl = new StringBuilder(url.toExternalForm());
        int index = jarUrl.lastIndexOf(".jar");
        if (index >= 0) {
            jarUrl.setLength(index + 4);
            if (log.isDebugEnabled()) {
                log.debug("Extracted JAR URL:" + jarUrl);
            }
        } else {
            if (log.isDebugEnabled()) {
                log.debug("Not a JAR:" + jarUrl);
            }
            return null;
        }
// Try to open and test it
        try {
            URL testUrl = new URL(jarUrl.toString());
            if (isJar(testUrl)) {
                return testUrl;
            } else {
// WebLogic fix: check if the URL's file exists in the filesystem.
                if (log.isDebugEnabled()) {
                    log.debug("Not a JAR:" + jarUrl);
                }
                jarUrl.replace(0, jarUrl.length(), testUrl.getFile());
                File file = new File(jarUrl.toString());
// File name might be URL-encoded
                if (!file.exists()) {
                    try {
                        file = new File(URLEncoder.encode(jarUrl.toString(), "UTF-8"));
                    } catch (UnsupportedEncodingException e) {
                        throw new RuntimeException("Unsupported encoding?  UTF-8?  That's unpossible.");
                    }
                }
                if (file.exists()) {
                    if (log.isDebugEnabled()) {
                        log.debug("Trying real file:" + file.getAbsolutePath());
                    }
                    testUrl = file.toURI().toURL();
                    if (isJar(testUrl)) {
                        return testUrl;
                    }
                }
            }
        } catch (MalformedURLException e) {
            log.warn("Invalid JAR URL:" + jarUrl);
        }
        if (log.isDebugEnabled()) {
            log.debug("Not a JAR:" + jarUrl);
        }
        return null;
    }
    protected boolean isNestedJar(URL url) {
        if (!"jar".equals(url.getProtocol()))
        {
            return false;
        }
        String urlStr = url.toExternalForm();
        int indexOfWar = urlStr.indexOf(".war!/");
        int indexOfJar = urlStr.indexOf(".jar!/");
        if (indexOfWar != -1) { // Executable War
            return indexOfWar != urlStr.lastIndexOf(".jar!/");
        } else { // Executable Jar
            return indexOfJar != urlStr.lastIndexOf(".jar!/");
        }
    }
    /**
     * Converts a Java package name to a path that can be looked up with a call to
     * <p>
     * {@link ClassLoader#getResources(String)}.
     *
     * @param packageName The Java package name to convert to a path
     */
    protected String getPackagePath(String packageName) {
        return packageName == null ? null : packageName.replace('.', '/');
    }
    /**
     * Returns true if the resource located at the given URL is a JAR file.
     *
     * @param url The URL of the resource to test.
     */
    protected boolean isJar(URL url) {
        return isJar(url, new byte[JAR_MAGIC.length]);
    }
    /**
     * Returns true if the resource located at the given URL is a JAR file.
     *
     * @param url    The URL of the resource to test.
     * @param buffer A buffer into which the first few bytes of the resource are read. The buffer
     *               <p>
     *               must be at least the size of {@link #JAR_MAGIC}. (The same buffer may be reused
     *               <p>
     *               for multiple calls as an optimization.)
     */
    protected boolean isJar(URL url, byte[] buffer) {
        InputStream is = null;
        try {
            is = url.openStream();
            is.read(buffer, 0, JAR_MAGIC.length);
            if (Arrays.equals(buffer, JAR_MAGIC)) {
                if (log.isDebugEnabled()) {
                    log.debug("Found JAR:" + url);
                }
                return true;
            }
        } catch (Exception e) {
// Failure to read the stream means this is not a JAR
        } finally {
            if (is != null) {
                try {
                    is.close();
                } catch (Exception e) {
// Ignore
                }
            }
        }
        return false;
    }
}
